markdown-text-editor 2.0.0-beta.0 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +78 -4
  2. package/dist/markdown-text-editor.es.js +23 -16
  3. package/dist/markdown-text-editor.es.js.map +1 -1
  4. package/dist/markdown-text-editor.min.js +6 -6
  5. package/dist/markdown-text-editor.min.js.map +1 -1
  6. package/dist/markdown-text-editor.umd.js +6 -6
  7. package/dist/markdown-text-editor.umd.js.map +1 -1
  8. package/locale/ar.js +55 -0
  9. package/locale/az.js +55 -0
  10. package/locale/bg.js +55 -0
  11. package/locale/bn.js +55 -0
  12. package/locale/bs.js +55 -0
  13. package/locale/ca.js +55 -0
  14. package/locale/cs.js +55 -0
  15. package/locale/da.js +55 -0
  16. package/locale/de.js +55 -0
  17. package/locale/el.js +55 -0
  18. package/locale/es.js +55 -0
  19. package/locale/et.js +55 -0
  20. package/locale/fa.js +55 -0
  21. package/locale/fi.js +55 -0
  22. package/locale/fil.js +55 -0
  23. package/locale/fr.js +55 -0
  24. package/locale/ga.js +55 -0
  25. package/locale/he.js +55 -0
  26. package/locale/hi.js +55 -0
  27. package/locale/hr.js +55 -0
  28. package/locale/hu.js +55 -0
  29. package/locale/hy.js +55 -0
  30. package/locale/id.js +55 -0
  31. package/locale/is.js +55 -0
  32. package/locale/it.js +55 -0
  33. package/locale/ja.js +55 -0
  34. package/locale/kn.js +55 -0
  35. package/locale/ko.js +55 -0
  36. package/locale/locale.d.ts +10 -0
  37. package/locale/lt.js +55 -0
  38. package/locale/lv.js +55 -0
  39. package/locale/mk.js +55 -0
  40. package/locale/mr.js +55 -0
  41. package/locale/ms.js +55 -0
  42. package/locale/mt.js +55 -0
  43. package/locale/nl.js +55 -0
  44. package/locale/no.js +55 -0
  45. package/locale/pl.js +55 -0
  46. package/locale/pt.js +55 -0
  47. package/locale/ro.js +55 -0
  48. package/locale/ru.js +55 -0
  49. package/locale/sk.js +55 -0
  50. package/locale/sl.js +55 -0
  51. package/locale/sq.js +55 -0
  52. package/locale/sr.js +55 -0
  53. package/locale/sv.js +55 -0
  54. package/locale/sw.js +55 -0
  55. package/locale/ta.js +55 -0
  56. package/locale/te.js +55 -0
  57. package/locale/template.js +62 -0
  58. package/locale/th.js +55 -0
  59. package/locale/tr.js +55 -0
  60. package/locale/uk.js +55 -0
  61. package/locale/ur.js +55 -0
  62. package/locale/uz.js +55 -0
  63. package/locale/vi.js +55 -0
  64. package/locale/zh-hk.js +55 -0
  65. package/locale/zh.js +55 -0
  66. package/package.json +13 -2
  67. package/types/index.d.ts +16 -5
package/README.md CHANGED
@@ -13,7 +13,7 @@ A lightweight, embeddable JavaScript Markdown editor that transforms a standard
13
13
 
14
14
  A native-first Markdown editor built on a standard textarea. No data binding, no API — just drop it in and your forms keep working as-is.
15
15
 
16
- **Works standalone.** No Frutjam, no Tailwind, no framework required, the styles are bundled.
16
+ **Works standalone.** No Frutjam, no Tailwind, no framework required. One script and one stylesheet.
17
17
 
18
18
  ![Hybrid mode: headings, bold text and list markers styled live inside the textarea while the Markdown syntax stays visible](https://cdn.frutjam.com/media/plugins/hybrid-mode-markdown-editor.webp)
19
19
 
@@ -40,7 +40,7 @@ Most JavaScript markdown editors (EasyMDE, SimpleMDE, CodeMirror-based editors)
40
40
  | Swap the markdown parser | ✅ | ❌ |
41
41
  | Keyboard shortcuts | ✅ | Partial |
42
42
  | Dark mode / theming | ✅ | Limited |
43
- | Bundle size, gzipped | 54 KB | 107 KB (JS + CSS) |
43
+ | Bundle size, gzipped | 56 KB (JS + CSS) | 107 KB (JS + CSS) |
44
44
 
45
45
  ## 🚀 Quick Start
46
46
 
@@ -90,6 +90,22 @@ Import the stylesheet **or** link it, never both. If you forget it entirely the
90
90
 
91
91
  That's it. Form submission, `.value` access, and all native textarea behaviour work exactly as before.
92
92
 
93
+ ### Upgrading from 1.x
94
+
95
+ **Add the stylesheet.** It used to be carried inside the JavaScript and injected as a `<style>` element. It is now a real file, so an existing setup keeps working but renders unstyled until you link or import it. The editor says so in the console rather than leaving you to guess.
96
+
97
+ ```javascript
98
+ import 'markdown-text-editor/style.css'; // bundlers
99
+ ```
100
+
101
+ ```html
102
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/markdown-text-editor/dist/markdown-text-editor.css">
103
+ ```
104
+
105
+ If you load the editor from an unpinned CDN URL, this reaches you the moment 2.0.0 publishes. Pin a version if you are not ready.
106
+
107
+ That is the whole breaking change. `labels` was renamed to `locale` and still works under the old name, so nothing else needs touching.
108
+
93
109
  ## ✨ Features
94
110
 
95
111
  - 🔌 **Native Form Integration** — Works exactly like a standard `<textarea>`. No complex APIs — just use `.value` or the `name` attribute. Compatible with Django, Laravel, PHP, Rails, Node.js, and with React or Vue via a ref and `destroy()` on unmount
@@ -106,12 +122,14 @@ That's it. Form submission, `.value` access, and all native textarea behaviour w
106
122
  - 🛡️ **XSS Safe** — Preview output sanitized via [DOMPurify](https://github.com/cure53/DOMPurify) before rendering, including the output of a custom renderer
107
123
  - 🛡️ **CSP Compatible** — No inline event handlers, no `eval`, and no injected `<style>` element. The stylesheet is a real file you link, so a strict policy needs no `'unsafe-inline'` exception for either `script-src` or `style-src` ([#42](https://github.com/nezanuha/markdown-text-editor/issues/42))
108
124
  - 🌍 **RTL Support** — Native Right-to-Left support for Arabic, Urdu, Farsi, and other RTL languages
125
+ - 🗣️ **Translatable Interface** — Every tooltip, menu item, dialog field and screen reader name goes through the `locale` option, keyed by the English string. `locale/template.js` lists all 45 of them so you don't have to go looking. Fifty-six languages ship with the package; you import the one you need
109
126
  - 🌙 **Dark Mode & Theming** — Inherits `data-theme` from any ancestor element. Built-in light, dark, snowberry, and darkberry themes. Fully customizable via CSS variables
110
127
  - 🧩 **Custom Tools** — Add your own toolbar button with `{ custom: { title, icon, action } }`, or extend `MarkdownEditor.Tool` when it builds its own markup. Keyboard shortcuts, translation and teardown work exactly as they do for the built-ins. No fork, no patching
111
128
  - 🎛️ **Modular Toolbar** — Pick exactly which tools appear and in what order
112
129
  - 🟦 **TypeScript Ready** — Definitions ship with the package. Options, toolbar entries and variable shapes are all checked, so a mistyped tool name is a compile error rather than a silently missing button
130
+ - 🖥️ **SSR Safe** — Importing it on the server doesn't throw, so Next.js, Remix and Nuxt need no `dynamic(..., { ssr: false })` wrapper. The `<textarea>` is in the server HTML with its content, and the editor enhances it after hydration, so there's no markup mismatch and nothing pops in
113
131
  - 📦 **Universal Module Support** — ESM, CommonJS, UMD, and IIFE. Works with Vite, webpack, Rollup, or directly via `<script src>` CDN — no configuration needed
114
- - 🚀 **High Performance** — ~40KB gzipped JS plus ~13KB gzipped CSS, cached separately. Debounced preview, cached layout calculations, conflict-free Tab/Enter handling for large documents
132
+ - 🚀 **High Performance** — ~43KB gzipped JS plus ~14KB gzipped CSS, cached separately. Debounced preview, cached layout calculations, conflict-free Tab/Enter handling for large documents
115
133
 
116
134
  ## 🛠 Developer Workflow
117
135
 
@@ -189,6 +207,53 @@ new MarkdownEditor('#markdown-editor', { toolbar: ['bold', EmbedTool, 'preview']
189
207
 
190
208
  Two helpers exist for tools that need them: `editor.renderMarkdown(md)` returns sanitized HTML exactly as the preview does, and `editor.createToolbar(textarea, ['bold', 'italic'])` builds a real toolbar bound to another textarea, for a tool collecting rich text in its own dialog.
191
209
 
210
+ ### Translate the interface
211
+
212
+ Every tooltip, menu item, dialog field and screen reader name goes through the `locale` option, keyed by the English string. Anything you leave out stays in English.
213
+
214
+ ```javascript
215
+ import fr from 'markdown-text-editor/locale/fr.js';
216
+
217
+ new MarkdownEditor('#markdown-editor', { locale: fr });
218
+ ```
219
+
220
+ `locale` takes the strings, not a language code. `locale: 'fr'` is a compile error in TypeScript and logs what to write instead in JavaScript. Importing the one language you want is why nobody downloads the other fifty-five.
221
+
222
+ <details>
223
+ <summary><strong>56 languages ship with the package</strong></summary>
224
+
225
+ `ar` Arabic · `az` Azerbaijani · `bg` Bulgarian · `bn` Bengali · `bs` Bosnian · `ca` Catalan · `cs` Czech · `da` Danish · `de` German · `el` Greek · `es` Spanish · `et` Estonian · `fa` Persian · `fi` Finnish · `fil` Filipino · `fr` French · `ga` Irish · `he` Hebrew · `hi` Hindi · `hr` Croatian · `hu` Hungarian · `hy` Armenian · `id` Indonesian · `is` Icelandic · `it` Italian · `ja` Japanese · `kn` Kannada · `ko` Korean · `lt` Lithuanian · `lv` Latvian · `mk` Macedonian · `mr` Marathi · `ms` Malay · `mt` Maltese · `nl` Dutch · `no` Norwegian · `pl` Polish · `pt` Portuguese · `ro` Romanian · `ru` Russian · `sk` Slovak · `sl` Slovenian · `sq` Albanian · `sr` Serbian · `sv` Swedish · `sw` Swahili · `ta` Tamil · `te` Telugu · `th` Thai · `tr` Turkish · `uk` Ukrainian · `ur` Urdu · `uz` Uzbek · `vi` Vietnamese · `zh` Chinese (Simplified) · `zh-hk` Chinese (Hong Kong)
226
+
227
+ English needs no file; it is what the editor falls back to.
228
+
229
+ None of these has been checked by a native speaker yet, so each file exports `reviewed = false` and says so in its header. If one reads wrong to you, [a pull request](https://github.com/nezanuha/markdown-text-editor/issues) correcting a single line is welcome and genuinely useful. Languages with more than one plural form, such as Polish and Arabic, currently get one wording that reads correctly for most counts but not all.
230
+
231
+ </details>
232
+
233
+ Partial is fine, and you can override a shipped translation without forking it:
234
+
235
+ ```javascript
236
+ import fr from 'markdown-text-editor/locale/fr.js';
237
+
238
+ new MarkdownEditor('#markdown-editor', {
239
+ locale: { ...fr, 'Image': 'Photo' } // your wording wins
240
+ });
241
+ ```
242
+
243
+ **`locale/template.js` lists every string**, English on both sides. Copy it to add a language, or map it through a translation system you already have, which keeps the editor's wording consistent with the rest of your interface:
244
+
245
+ ```javascript
246
+ import template from 'markdown-text-editor/locale/template.js';
247
+
248
+ const locale = Object.fromEntries(
249
+ Object.keys(template).map(key => [key, i18n.t(`editor.${key}`)])
250
+ );
251
+
252
+ new MarkdownEditor('#markdown-editor', { locale });
253
+ ```
254
+
255
+ Counts carry a `{n}` placeholder, so `'{n} words'` can become `'{n} mots'` or `'mots : {n}'` and the number lands where that language wants it. CSS class names never change, so `.bold-btn` stays `.bold-btn` in every language.
256
+
192
257
  ### Tear down in SPAs
193
258
 
194
259
  ```javascript
@@ -199,8 +264,15 @@ editor.destroy();
199
264
  In React, create the editor in an effect and return `destroy` as the cleanup — that also covers StrictMode running effects twice in development:
200
265
 
201
266
  ```jsx
267
+ // Options are read once, so reach the latest onChange through a ref rather
268
+ // than rebuilding the editor whenever the parent re-renders
269
+ const onChangeRef = useRef(onChange);
270
+ onChangeRef.current = onChange;
271
+
202
272
  useEffect(() => {
203
- const editor = new MarkdownEditor(ref.current, { onChange });
273
+ const editor = new MarkdownEditor(ref.current, {
274
+ onChange: (value) => onChangeRef.current?.(value),
275
+ });
204
276
  return () => editor.destroy();
205
277
  }, []);
206
278
  ```
@@ -209,6 +281,8 @@ Use `defaultValue`, not `value`. The editor writes to the textarea directly, so
209
281
 
210
282
  The same two rules cover Svelte, Angular and anything else: don't bind the value, and call `destroy()` on unmount. In Angular, `onChange` fires outside the zone, so wrap it in `zone.run()` for change detection to notice.
211
283
 
284
+ This is also all you need for server-side rendering. `useEffect`, `onMounted` and `onMount` never run on the server, so the DOM work stays in the browser and importing the package server-side is safe. Next.js, Remix and Nuxt need no `ssr: false` wrapper, and the `<textarea>` ships in the server HTML with its content already in it.
285
+
212
286
  Options are read once when the editor is constructed. Changing them later has no effect — destroy the editor and create a new one instead.
213
287
 
214
288
  ## 📖 Documentation
@@ -2866,20 +2866,23 @@ var Yn = "\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"
2866
2866
  return this.previewContent;
2867
2867
  }
2868
2868
  }, er = class {
2869
- constructor(e, t = {}) {
2870
- this.options = t, this.footer = document.createElement("div"), this.footer.className = "editor-footer not-prose fj:me-surface fj:me-surface-outline fj:border-base-soft fj:border-0 fj:border-t fj:flex fj:justify-between fj:items-center fj:px-3 fj:py-1 fj:text-xs fj:opacity-60 fj:select-none", this.footer.setAttribute("aria-hidden", "true"), this.positionEl = document.createElement("span"), this.charsEl = document.createElement("span"), this.wordsEl = document.createElement("span"), (this.options.line || this.options.col) && this.footer.appendChild(this.positionEl), this.options.chars && this.footer.appendChild(this.charsEl), this.options.words && this.footer.appendChild(this.wordsEl), e.appendChild(this.footer);
2869
+ constructor(e, t = {}, n = (e) => e) {
2870
+ this.options = t, this.label = n, this.footer = document.createElement("div"), this.footer.className = "editor-footer not-prose fj:me-surface fj:me-surface-outline fj:border-base-soft fj:border-0 fj:border-t fj:flex fj:justify-between fj:items-center fj:px-3 fj:py-1 fj:text-xs fj:opacity-60 fj:select-none", this.footer.setAttribute("aria-hidden", "true"), this.positionEl = document.createElement("span"), this.charsEl = document.createElement("span"), this.wordsEl = document.createElement("span"), (this.options.line || this.options.col) && this.footer.appendChild(this.positionEl), this.options.chars && this.footer.appendChild(this.charsEl), this.options.words && this.footer.appendChild(this.wordsEl), e.appendChild(this.footer);
2871
2871
  }
2872
2872
  update(e) {
2873
2873
  let t = e.value, n = e.selectionStart ?? 0;
2874
2874
  if (this.options.line || this.options.col) {
2875
2875
  let e = t.substring(0, n).split("\n"), r = [];
2876
- this.options.line && r.push(`Ln ${e.length}`), this.options.col && r.push(`Col ${e[e.length - 1].length + 1}`), this.positionEl.textContent = r.join(", ");
2876
+ this.options.line && r.push(this._count("Ln {n}", e.length)), this.options.col && r.push(this._count("Col {n}", e[e.length - 1].length + 1)), this.positionEl.textContent = r.join(", ");
2877
2877
  }
2878
- if (this.options.chars && (this.charsEl.textContent = `${t.length.toLocaleString()} chars`), this.options.words) {
2878
+ if (this.options.chars && (this.charsEl.textContent = this._count("{n} chars", t.length)), this.options.words) {
2879
2879
  let e = t.trim() ? t.trim().split(/\s+/).length : 0;
2880
- this.wordsEl.textContent = `${e.toLocaleString()} words`;
2880
+ this.wordsEl.textContent = this._count("{n} words", e);
2881
2881
  }
2882
2882
  }
2883
+ _count(e, t) {
2884
+ return this.label(e).replace("{n}", t.toLocaleString());
2885
+ }
2883
2886
  }, tr = class {
2884
2887
  diff(e, t, n = {}) {
2885
2888
  let r;
@@ -3574,33 +3577,33 @@ var Dr = class {
3574
3577
  }
3575
3578
  _buildPanel() {
3576
3579
  let e = document.createElement("div");
3577
- e.className = "find-replace-panel fj:absolute fj:top-1 fj:right-1 fj:z-20 fj:me-surface fj:me-surface-outline fj:me-surface-rounded fj:p-1.5 fj:flex fj:flex-col fj:gap-1 fj:shadow-lg", e.setAttribute("role", "search"), e.setAttribute("aria-label", "Find and replace");
3580
+ e.className = "find-replace-panel fj:absolute fj:top-1 fj:right-1 fj:z-20 fj:me-surface fj:me-surface-outline fj:me-surface-rounded fj:p-1.5 fj:flex fj:flex-col fj:gap-1 fj:shadow-lg", e.setAttribute("role", "search"), e.setAttribute("aria-label", this.editor.label("Find and replace"));
3578
3581
  let t = document.createElement("div");
3579
- t.className = "fj:flex fj:items-center fj:gap-1", this._findInput = document.createElement("input"), this._findInput.type = "text", this._findInput.placeholder = "Find", this._findInput.className = "fj:me-input fj:me-input-xs fj:w-36", this._findInput.setAttribute("aria-label", "Find"), this._findInput.addEventListener("input", () => this._search()), this._findInput.addEventListener("keydown", (e) => {
3582
+ t.className = "fj:flex fj:items-center fj:gap-1", this._findInput = document.createElement("input"), this._findInput.type = "text", this._findInput.placeholder = this.editor.label("Find"), this._findInput.className = "fj:me-input fj:me-input-xs fj:w-36", this._findInput.setAttribute("aria-label", this.editor.label("Find")), this._findInput.addEventListener("input", () => this._search()), this._findInput.addEventListener("keydown", (e) => {
3580
3583
  e.key === "Enter" && (e.shiftKey ? this._prev() : this._next()), e.key === "Escape" && (e.preventDefault(), this.close());
3581
3584
  }), this._countEl = document.createElement("span"), this._countEl.className = "fj:text-xs fj:opacity-50 fj:whitespace-nowrap fj:min-w-[3.5rem] fj:text-center fj:select-none", this._countEl.setAttribute("aria-live", "polite");
3582
3585
  let n = this._iconBtn("Previous match", "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m18 15-6-6-6 6\"/></svg>");
3583
3586
  n.addEventListener("click", () => this._prev());
3584
3587
  let r = this._iconBtn("Next match", "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg>");
3585
- r.addEventListener("click", () => this._next()), this._caseSensitiveBtn = document.createElement("button"), this._caseSensitiveBtn.type = "button", this._caseSensitiveBtn.className = "fj:me-btn fj:me-btn-xs fj:me-btn-square fj:me-btn-ghost fj:text-xs fj:font-bold", this._caseSensitiveBtn.textContent = "Aa", this._caseSensitiveBtn.setAttribute("aria-label", "Match case"), this._caseSensitiveBtn.setAttribute("aria-pressed", "false"), this._caseSensitiveBtn.title = "Match case", this._caseSensitiveBtn.addEventListener("click", () => {
3588
+ r.addEventListener("click", () => this._next()), this._caseSensitiveBtn = document.createElement("button"), this._caseSensitiveBtn.type = "button", this._caseSensitiveBtn.className = "fj:me-btn fj:me-btn-xs fj:me-btn-square fj:me-btn-ghost fj:text-xs fj:font-bold", this._caseSensitiveBtn.textContent = "Aa", this._caseSensitiveBtn.setAttribute("aria-label", this.editor.label("Match case")), this._caseSensitiveBtn.setAttribute("aria-pressed", "false"), this._caseSensitiveBtn.title = this.editor.label("Match case"), this._caseSensitiveBtn.addEventListener("click", () => {
3586
3589
  this._caseSensitive = !this._caseSensitive, this._caseSensitiveBtn.setAttribute("aria-pressed", String(this._caseSensitive)), this._caseSensitiveBtn.classList.toggle("fj:me-btn-active", this._caseSensitive), this._search();
3587
3590
  });
3588
3591
  let i = document.createElement("button");
3589
- i.type = "button", i.className = "fj:me-btn fj:me-btn-xs fj:me-btn-ghost fj:text-xs fj:px-1.5", i.textContent = "Replace", i.setAttribute("aria-label", "Toggle replace row"), i.addEventListener("click", () => {
3592
+ i.type = "button", i.className = "fj:me-btn fj:me-btn-xs fj:me-btn-ghost fj:text-xs fj:px-1.5", i.textContent = this.editor.label("Replace"), i.setAttribute("aria-label", this.editor.label("Toggle replace row")), i.addEventListener("click", () => {
3590
3593
  this._replaceRow.hidden = !this._replaceRow.hidden, this._replaceRow.hidden || this._replaceInput.focus();
3591
3594
  });
3592
3595
  let a = this._iconBtn("Close", "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/></svg>");
3593
- a.addEventListener("click", () => this.close()), t.append(this._findInput, this._countEl, n, r, this._caseSensitiveBtn, i, a), this._replaceRow = document.createElement("div"), this._replaceRow.className = "fj:flex fj:items-center fj:gap-1", this._replaceRow.hidden = !0, this._replaceInput = document.createElement("input"), this._replaceInput.type = "text", this._replaceInput.placeholder = "Replace", this._replaceInput.className = "fj:me-input fj:me-input-xs fj:w-36", this._replaceInput.setAttribute("aria-label", "Replace with"), this._replaceInput.addEventListener("keydown", (e) => {
3596
+ a.addEventListener("click", () => this.close()), t.append(this._findInput, this._countEl, n, r, this._caseSensitiveBtn, i, a), this._replaceRow = document.createElement("div"), this._replaceRow.className = "fj:flex fj:items-center fj:gap-1", this._replaceRow.hidden = !0, this._replaceInput = document.createElement("input"), this._replaceInput.type = "text", this._replaceInput.placeholder = this.editor.label("Replace"), this._replaceInput.className = "fj:me-input fj:me-input-xs fj:w-36", this._replaceInput.setAttribute("aria-label", this.editor.label("Replace with")), this._replaceInput.addEventListener("keydown", (e) => {
3594
3597
  e.key === "Enter" && this._replace(), e.key === "Escape" && (e.preventDefault(), this.close());
3595
3598
  });
3596
3599
  let o = document.createElement("button");
3597
- o.type = "button", o.className = "fj:me-btn fj:me-btn-xs fj:me-btn-ghost fj:text-xs fj:px-1.5", o.textContent = "Replace", o.addEventListener("click", () => this._replace());
3600
+ o.type = "button", o.className = "fj:me-btn fj:me-btn-xs fj:me-btn-ghost fj:text-xs fj:px-1.5", o.textContent = this.editor.label("Replace"), o.addEventListener("click", () => this._replace());
3598
3601
  let s = document.createElement("button");
3599
- s.type = "button", s.className = "fj:me-btn fj:me-btn-xs fj:me-btn-ghost fj:text-xs fj:px-1.5", s.textContent = "All", s.setAttribute("aria-label", "Replace all"), s.addEventListener("click", () => this._replaceAll()), this._replaceRow.append(this._replaceInput, o, s), e.append(t, this._replaceRow), this.editor.markdownEditorDiv.appendChild(e), this._panel = e;
3602
+ s.type = "button", s.className = "fj:me-btn fj:me-btn-xs fj:me-btn-ghost fj:text-xs fj:px-1.5", s.textContent = this.editor.label("All"), s.setAttribute("aria-label", this.editor.label("Replace all")), s.addEventListener("click", () => this._replaceAll()), this._replaceRow.append(this._replaceInput, o, s), e.append(t, this._replaceRow), this.editor.markdownEditorDiv.appendChild(e), this._panel = e;
3600
3603
  }
3601
3604
  _iconBtn(e, t) {
3602
3605
  let n = document.createElement("button");
3603
- n.type = "button", n.className = "fj:me-btn fj:me-btn-xs fj:me-btn-square fj:me-btn-ghost", n.setAttribute("aria-label", e), n.innerHTML = t;
3606
+ n.type = "button", n.className = "fj:me-btn fj:me-btn-xs fj:me-btn-square fj:me-btn-ghost", n.setAttribute("aria-label", this.editor.label(e)), n.innerHTML = t;
3604
3607
  let r = n.querySelector("svg");
3605
3608
  return r && r.setAttribute("aria-hidden", "true"), n;
3606
3609
  }
@@ -3760,13 +3763,17 @@ var Dr = class {
3760
3763
  }
3761
3764
  }, jr = new rt({ breaks: !0 }), Mr = class e {
3762
3765
  constructor(e, t = {}) {
3763
- this.usertextarea = typeof e == "string" ? document.querySelector(e) : e, this.options = t, this.mode = t.mode || "plain", this.preview = !this.options.toolbar || this.options.toolbar.includes("preview"), this.footerOptions = this._parseFooterOptions(t.footer), this.variables = this._parseVariables(t.toolbar), this.imageUpload = this._parseImageUpload(t.toolbar), this.previewTimer = null, this.init(), this.undoRedoManager = new Sr(this), this.listManager = new wr(this), this.indentManager = new Cr(this), this.shortcutManager = new Dr(this), this.findReplace = new Or(this), this.pasteManager = new Ar(this), this._warnIfStylesheetMissing();
3766
+ this.usertextarea = typeof e == "string" ? document.querySelector(e) : e, this.options = t, this.mode = t.mode || "plain", this.preview = !this.options.toolbar || this.options.toolbar.includes("preview"), this.footerOptions = this._parseFooterOptions(t.footer), this.variables = this._parseVariables(t.toolbar), this.imageUpload = this._parseImageUpload(t.toolbar), this.previewTimer = null, this.init(), this.undoRedoManager = new Sr(this), this.listManager = new wr(this), this.indentManager = new Cr(this), this.shortcutManager = new Dr(this), this.findReplace = new Or(this), this.pasteManager = new Ar(this), this._warnIfStylesheetMissing(), this._warnIfLocaleIsACode();
3767
+ }
3768
+ _warnIfLocaleIsACode() {
3769
+ let e = this.options.locale;
3770
+ typeof e == "string" && console.error(`[MarkdownEditor] locale: '${e}' has no effect. It takes the strings, not a code:\n import ${/^[a-z]{2}$/i.test(e) ? e : "strings"} from 'markdown-text-editor/locale/${e}.js'\n new MarkdownEditor(el, { locale: ${/^[a-z]{2}$/i.test(e) ? e : "strings"} })`);
3764
3771
  }
3765
3772
  _warnIfStylesheetMissing() {
3766
3773
  e._cssWarned || this.editorContainer?.isConnected && (getComputedStyle(this.editorContainer).getPropertyValue("--mte-css").trim() || (e._cssWarned = !0, console.error("[MarkdownEditor] Stylesheet not loaded, so the editor will look unstyled.\n Bundlers: import 'markdown-text-editor/style.css'\n Browsers: <link rel=\"stylesheet\" href=\".../dist/markdown-text-editor.css\">")));
3767
3774
  }
3768
3775
  label(e) {
3769
- return this.options.labels?.[e] ?? e;
3776
+ return (this.options.locale ?? this.options.labels)?.[e] ?? e;
3770
3777
  }
3771
3778
  _parseImageUpload(e) {
3772
3779
  let t = (Array.isArray(e) ? e : []).find((e) => e && typeof e == "object" && e.image)?.image?.fileInput;
@@ -3867,7 +3874,7 @@ var Dr = class {
3867
3874
  addFooter() {
3868
3875
  if (!this.footerOptions) return;
3869
3876
  let { line: e, col: t, chars: n, words: r } = this.footerOptions;
3870
- (e || t || n || r) && (this.footer = new er(this.editorContainer, this.footerOptions), this.footer.update(this.usertextarea), this._footerUpdate = () => this.footer.update(this.usertextarea), this.usertextarea.addEventListener("input", this._footerUpdate), this.usertextarea.addEventListener("click", this._footerUpdate), this.usertextarea.addEventListener("keyup", this._footerUpdate), this.usertextarea.addEventListener("focus", this._footerUpdate));
3877
+ (e || t || n || r) && (this.footer = new er(this.editorContainer, this.footerOptions, (e) => this.label(e)), this.footer.update(this.usertextarea), this._footerUpdate = () => this.footer.update(this.usertextarea), this.usertextarea.addEventListener("input", this._footerUpdate), this.usertextarea.addEventListener("click", this._footerUpdate), this.usertextarea.addEventListener("keyup", this._footerUpdate), this.usertextarea.addEventListener("focus", this._footerUpdate));
3871
3878
  }
3872
3879
  addToolbar() {
3873
3880
  this.toolbarInstance = new Qn(this, this.options.toolbar || [